/*
 * FindBugs - Find Bugs in Java programs
 * Copyright (C) 2006, University of Maryland
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.ba.generic;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.bcel.generic.ArrayType;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.ReferenceType;
import org.apache.bcel.generic.Type;

import edu.umd.cs.findbugs.ba.type.NullType;
import edu.umd.cs.findbugs.util.Util;

/**
 * Utilities for adding support for generics. Most of these
 * methods can be applied to generic and non generic type 
 * information.
 * 
 * @author Nat Ayewah
 */
public class GenericUtilities {

	public static enum TypeCategory {
		

		/** A simple (non-generic ObjectType) */
		 PLAIN_OBJECT_TYPE 
		 {
			@Override
			public ReferenceType produce(GenericObjectType obj) {
				return obj;
			}
			@Override
			public String asString(GenericObjectType obj) {
				// obj.getTypeCategory() does not return PLAIN_OBJECT_TYPE
				return GenericUtilities.getString(obj);
			}
		},

		/** A array */
		ARRAY_TYPE
		 {
			@Override
			public ReferenceType produce(GenericObjectType obj) {
				return obj;
			}
			@Override
			public String asString(GenericObjectType obj) {
				// obj.getTypeCategory() does not return ARRAY_TYPE
				return GenericUtilities.getString(obj);
			}
		},

		/** A parameterized class e.g. <code>List&lt;String&gt;</code> */
		PARAMETERIZED 
		 {
			@Override
			public ReferenceType produce(GenericObjectType obj) {
				return obj;
			}
			@Override
			public String asString(GenericObjectType obj) {
				StringBuilder b = new StringBuilder(obj.toPlainString());
				b.append("<");
				boolean first = true;
				for (Type t : obj.parameters) {
					if (!first)
						b.append(",");
					first = false;
					b.append(GenericUtilities.getString(t));
				}
				b.append(">");
				return b.toString();				
			}
		},

		/** A simple type variable e.g. <code>E</code>. 
		 *  Underlying ObjectType is <code>java.lang.Object</code> */
		TYPE_VARIABLE 
		 {
			@Override
			public ReferenceType produce(GenericObjectType obj) {
				return ObjectType.OBJECT;
			}
			@Override
			public String asString(GenericObjectType obj) {				
				return obj.variable;				
			}
		},

		/** A simple wildcard i.e. <code>?</code>. 
		 *  Underlying ObjectType is <code>java.lang.Object</code> */
		 WILDCARD 
		 {
			@Override
			public ReferenceType produce(GenericObjectType obj) {
				return ObjectType.OBJECT;
			}
			@Override
			public String asString(GenericObjectType obj) {				
				return "?";				
			}
		},

		/** A wildcard that extends another ObjectType e.g. <code>? extends Comparable</code>. 
		 *  Underlying ObjectType is <code>java.lang.Object</code>.
		 *  The extended type can be an ObjectType or a GenericObjectType */
		WILDCARD_EXTENDS 
		 {
			@Override
			public ReferenceType produce(GenericObjectType obj) {
				return obj.extension;
			}
			@Override
			public String asString(GenericObjectType obj) {				
				Type extension = obj.extension;
				assert extension != null;
				return "? extends " + GenericUtilities.getString(extension);
			}
		},

		/** A wildcard that is extended by another ObjectType e.g. <code>? super Comparable</code>. 
		 *  Underlying ObjectType is <code>java.lang.Object</code>.
		 *  The super type can be an ObjectType or a GenericObjectType */
		 WILDCARD_SUPER 
		 {
			@Override
			public ReferenceType produce(GenericObjectType obj) {
				return ObjectType.OBJECT;
			}
			@Override
			public String asString(GenericObjectType obj) {	
				Type extension = obj.extension;
				assert extension != null;
				return "? super " + GenericUtilities.getString(extension);
			}
		};

		public abstract String asString(GenericObjectType obj);
		public abstract ReferenceType produce(GenericObjectType obj);

		public static String asString(ArrayType atype) {
			Type obj = atype.getBasicType();
			String result = GenericUtilities.getString(obj);
			return result + Util.repeat("[]", atype.getDimensions());
		}
	}

	/**
	 * Get the TypeCategory that represents this Object
	 * @see GenericUtilities.TypeCategory 
	 */
	public static final TypeCategory getTypeCategory(Type type) {
		if (type instanceof GenericObjectType)
			return ((GenericObjectType) type).getTypeCategory();

		if (type instanceof ObjectType || type instanceof NullType)
			return TypeCategory.PLAIN_OBJECT_TYPE;

		if (type instanceof ArrayType)
			return TypeCategory.ARRAY_TYPE;

		throw new IllegalArgumentException("Not a reference type: " + type);
	}

	public static final boolean isPlainObject(Type type) {
		return getTypeCategory(type) == TypeCategory.PLAIN_OBJECT_TYPE;
	}

	/**
	 * Get String representation of a Type including Generic information
	 */
	public static final String getString(Type type) {
		if (type instanceof GenericObjectType)
			return ((GenericObjectType) type).toString(true);
		else if (type instanceof ArrayType)
			return TypeCategory.asString((ArrayType) type);
		else
			return type.toString();
	}

	 static String stripAngleBrackets(String s) {
		if (s.indexOf('<') == -1) return s;
		StringBuilder result = new StringBuilder(s.length());
		int nesting = 0;
		boolean seenLeftBracket = false;
		for(int i = 0; i < s.length(); i++) {
			char c = s.charAt(i);
			if (c == '<') {
				nesting++;
				seenLeftBracket = true;
			}
			else if (c == '>') nesting--;
			else if (nesting == 0) {
				if (seenLeftBracket && c == '.')
					result.append('$');
				else result.append(c);
			}
		}
		return result.toString();
	}
	 
	 public static GenericObjectType getType(String className, List<? extends ReferenceType> parameters) {
		 return new GenericObjectType(className, parameters);	
	 }
	/**
	 * This method is analogous to <code>Type.getType(String)</code>, 
	 * except that it also accepts signatures with generic information.
	 * e.g. <code>Ljava/util/ArrayList&lt;TT;&gt;;</code> <p>
	 * 
	 * The signature should only contain one type. Use GenericSignatureParser
	 * to break up a signature with many types or call createTypes(String) to
	 * return a list of types
	 */
	public static  Type getType(String signature) {
		// ensure signature only has one type
		if (new GenericSignatureParser("(" + signature + ")V").getNumParameters() != 1)
			throw new IllegalArgumentException("the following signature does not " +
					"contain exactly one type: " + signature);
		int index = 0;

		if (signature.startsWith("L")) {
			index = lastMatchedLeftAngleBracket(signature);
			if (index < 0)
				return Type.getType(stripAngleBrackets(signature));

			String typeParameters = signature.substring(index+1, nextUnmatchedRightAngleBracket(signature, index+1));
			List<ReferenceType> parameters = GenericUtilities.getTypes(typeParameters);			
			String baseType = removeMatchedAngleBrackets(signature.substring(1,index)).replace('.', '$');
			return new GenericObjectType(baseType,	parameters);		

		} else if (signature.startsWith("T")) {
			// ignore the prefix "T" and the suffix ";"
			return new GenericObjectType(signature.substring(1,signature.length()-1));

		} else if (signature.startsWith("[")) {
			index++;
			while (signature.charAt(index) == '[') index++;
			return new ArrayType( getType(signature.substring(index)), index);

		} else if (signature.startsWith("*")) {
			return new GenericObjectType("*");

		} else if (signature.startsWith("+") || signature.startsWith("-")) {
			return new GenericObjectType(
					signature.substring(0,1), 
					(ReferenceType) getType(signature.substring(1)) );

		} else
			// assert signature contains no generic information
			return Type.getType(signature);
	}

	public static ObjectType  merge(Type t1, ObjectType t2) {
		if (t1 instanceof GenericObjectType) 
			return merge((GenericObjectType) t1, t2);
		return t2;
	}
	public static ObjectType  merge(GenericObjectType t1, ObjectType t2) {
		if (t2 instanceof GenericObjectType) 
			return t2;
		List<? extends ReferenceType> parameters = t1.getParameters();
		if (parameters == null) return t2;
		return new GenericObjectType(t2.getClassName(), parameters);
	}
	public static String removeMatchedAngleBrackets(String s) {
		int first = s.indexOf('<');
		if (first < 0) return s;
		StringBuilder result = new StringBuilder(s.substring(0, first));
		int pos = first;
		int nesting = 0;
		while (pos < s.length()) {
			char c = s.charAt(pos++);
			if (c == '<') nesting++;
			else if (c == '>') nesting--;
			else if (nesting == 0) result.append(c);
		}
		return result.toString();

	}
	public static int nextUnmatchedRightAngleBracket(String s, int startingAt) {
		int nesting = 0;
		int pos = startingAt;

		while (true) {
			if (pos < 0) return -1;
			char c = s.charAt(pos);
			if (c == '>') {
				if (nesting == 0) return pos;
				nesting--;
			} else if (c == '<') nesting++;
			pos++;
		}
	}
	public static int lastMatchedLeftAngleBracket(String s) {
		int nesting = 0;
		int pos = s.length()-2;

		while (true) {
			if (pos < 0) 
				return -1;
			char c = s.charAt(pos);
			if (c == '<') {
				nesting--;
				if (nesting == 0) return pos;
			} else if (c == '>') nesting++;
			else if (nesting == 0) return -1;
			pos--;
		}
	}
	/**
	 * Parse a bytecode signature that has 1 or more (possibly generic) types 
	 * and return a list of the Types.
	 * @param signature bytecode signature e.g. 
	 * e.g. <code>Ljava/util/ArrayList&lt;Ljava/lang/String;&gt;;Ljava/util/ArrayList&lt;TT;&gt;;Ljava/util/ArrayList&lt;*&gt;;</code>
	 */
	public static final List<ReferenceType> getTypes(String signature) {
		GenericSignatureParser parser = new GenericSignatureParser("(" + signature + ")V");
		List<ReferenceType> types = new ArrayList<ReferenceType>();

		Iterator<String> iter = parser.parameterSignatureIterator();
		while (iter.hasNext()) {
			String parameterString = iter.next();
			types.add((ReferenceType)getType(parameterString));
		}
		return types;
	}

}
